package game.game.editors;

import com.crunch.core.Actor;
import com.crunch.core.Input;
import com.crunch.core.Renderable;
import com.crunch.graphics.gpu.GpuState;
import com.crunch.graphics.gpu.GpuTexture;
import com.crunch.math.Vector4f;
import game.game.RoomDefinition;
import game.game.RoomState;
import scenery.BackgroundLayerInstance;
import util.Camera;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.HashSet;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * Allows editing of background layers.
 */
public class BackgroundEditor extends Actor implements Renderable {
	public BackgroundEditor() {
		super(-50);
	}

	@Override
	public void onCreate() {
		getEngine().window.renderer.register(this);
		commandQueue = new ConcurrentLinkedQueue<Runnable>();

		// create list of textures
		Map<String, GpuTexture> textures = ((RoomState) getGameState()).getCoreState().resourceManager.getTextures();
		textureNames = new Object[textures.size()];
		int i = 0;
		for (String s : textures.keySet()) {
			textureNames[i] = s;
			++i;
		}

		editor = new BackgroundEditorFrame();

		eventListener = new Input.EventListener() {
			@Override
			public void processEvent(AWTEvent e, int eventType) {
				if (eventType == Input.MOUSE_EVENT) {
					MouseEvent ev = (MouseEvent) e;
					if (ev.getButton() == MouseEvent.BUTTON2) {
						// camera drag
						if (ev.getID() == MouseEvent.MOUSE_PRESSED) {
							RoomState roomState = (RoomState) getGameState();
							int scale = roomState.getCoreState().getWindowScale();
							draggingCamera = true;
							dragX = ev.getX() / scale;
							dragY = ev.getY() / scale;
						} else if (ev.getID() == MouseEvent.MOUSE_RELEASED) {
							draggingCamera = false;
						}
					}
				} else if (eventType == Input.MOUSE_MOTION_EVENT) {
					MouseEvent ev = (MouseEvent) e;
					if (draggingCamera) {
						RoomState roomState = (RoomState) getGameState();
						int scale = roomState.getCoreState().getWindowScale();
						int newDragX = ev.getX() / scale;
						int newDragY = ev.getY() / scale;
						int dx = newDragX - dragX;
						int dy = dragY - newDragY; // flip y

						Camera camera = roomState.getCamera();
						int newX = (int) camera.getX() - dx;
						int newY = (int) camera.getY() - dy;

						// clamp to room size
						int xMin = newX - camera.getViewportWidth()/2;
						int yMin = newY - camera.getViewportHeight()/2;
						int xMax = xMin + camera.getViewportWidth();
						int yMax = yMin + camera.getViewportHeight();
						if (xMax > roomState.getRoomWidth()) {
							int diff = xMax - roomState.getRoomWidth();
							newX -= diff;
							xMin -= diff;
						}
						if (yMax > roomState.getRoomHeight()) {
							int diff = yMax - roomState.getRoomHeight();
							newY -= diff;
							yMin -= diff;
						}
						if (xMin < 0) {
							int diff = -xMin;
							newX += diff;
						}
						if (yMin < 0) {
							int diff = -yMin;
							newY += diff;
						}

						camera.set((float) newX, (float) newY, camera.getZoom());

						dragX = newDragX;
						dragY = newDragY;
					}
				}
			}
		};

		eventListener.eventMask = Input.MOUSE_EVENT | Input.MOUSE_MOTION_EVENT;
		getEngine().input.addEventListener(eventListener);
	}

	// initializes editor with existing background layers
	public void init(RoomDefinition roomDefinition) {
		this.roomDefinition = roomDefinition;
		editor.init();
	}

	@Override
	public void onUpdate() {
		// run all commands
		Runnable cmd;
		while ((cmd = commandQueue.poll()) != null) {
			cmd.run();
		}
	}

	@Override
	public void onDestroy() {
		getEngine().window.renderer.unregister(this);
		editor.dispose();

		getEngine().input.removeEventListener(eventListener);
	}

	private RoomDefinition roomDefinition;
	private boolean draggingCamera = false;
	private int dragX, dragY;

	private Input.EventListener eventListener;

	private Object[] textureNames;

	@Override
	public void render(GpuState gpuState) {
		// nothing for now
	}

	@Override
	public int getDepth() {
		return Integer.MAX_VALUE - 10;
	}

	Queue<Runnable> commandQueue;

	BackgroundEditorFrame editor;
	private class BackgroundEditorFrame extends JFrame {
		private class Node {
			public BackgroundLayerInstance layer;
			public String text;
			public String toString() {
				return text;
			}
		}

		private JButton addButton;
		private JButton removeButton;
		private JList<Node> list;
		private DefaultListModel<Node> model;
		private JButton setTextureButton;
		private JSpinner depthSpinner;
		private JSpinner offsetXSpinner;
		private JSpinner offsetYSpinner;
		private JSpinner parallaxXSpinner;
		private JSpinner parallaxYSpinner;
		private JPanel colorChooser;
		private JSpinner alphaSpinner;

		private boolean performCallbacks;

		public BackgroundEditorFrame() {
			setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);

			setTitle("Background editor");

			JPanel contentPanel = new JPanel();
			add(contentPanel);

			GroupLayout layout = new GroupLayout(contentPanel);
			contentPanel.setLayout(layout);
			layout.setAutoCreateGaps(true);
			layout.setAutoCreateContainerGaps(true);

			addButton = new JButton("Add");
			removeButton = new JButton("Remove");

			model = new DefaultListModel<Node>();
			list = new JList<Node>(model);
			JScrollPane listScroll = new JScrollPane(list);

			setTextureButton = new JButton("Set texture");

			JLabel depthLabel = new JLabel("Depth:");
			depthSpinner = new JSpinner(new SpinnerNumberModel(0, Integer.MIN_VALUE, Integer.MAX_VALUE, 1));
			depthSpinner.setMinimumSize(new Dimension(32, depthSpinner.getMinimumSize().height));

			JLabel offsetLabel = new JLabel("Offset:");
			offsetXSpinner = new JSpinner(new SpinnerNumberModel(0, Integer.MIN_VALUE, Integer.MAX_VALUE, 1));
			offsetXSpinner.setMinimumSize(new Dimension(32, offsetXSpinner.getMinimumSize().height));
			offsetYSpinner = new JSpinner(new SpinnerNumberModel(0, Integer.MIN_VALUE, Integer.MAX_VALUE, 1));
			offsetYSpinner.setMinimumSize(new Dimension(32, offsetYSpinner.getMinimumSize().height));

			JLabel parallaxLabel = new JLabel("Parallax:");
			parallaxXSpinner = new JSpinner(new SpinnerNumberModel(
					new Float(0.0f), new Float(-Float.MAX_VALUE), new Float(1.0f), new Float(0.25f)));
			parallaxXSpinner.setMinimumSize(new Dimension(32, parallaxXSpinner.getMinimumSize().height));
			parallaxYSpinner = new JSpinner(new SpinnerNumberModel(
					new Float(0.0f), new Float(-Float.MAX_VALUE), new Float(1.0f), new Float(0.25f)));
			parallaxYSpinner.setMinimumSize(new Dimension(32, parallaxYSpinner.getMinimumSize().height));

			JLabel colorLabel = new JLabel("Color:");
			colorChooser = new JPanel();
			colorChooser.setPreferredSize(new Dimension(colorChooser.getPreferredSize().width, 16));
			colorChooser.setMinimumSize(new Dimension(colorChooser.getMinimumSize().width, 16));
			colorChooser.setMaximumSize(new Dimension(colorChooser.getMaximumSize().width, 16));
			colorChooser.setBorder(BorderFactory.createLineBorder(Color.BLACK));

			JLabel alphaLabel = new JLabel("A:");
			alphaSpinner = new JSpinner(new SpinnerNumberModel(
					new Float(1.0f), new Float(0.0f), new Float(1.0f), new Float(0.125f)));
			// keep alpha spinner small
			alphaSpinner.setMaximumSize(new Dimension(48, alphaSpinner.getMinimumSize().height));

			layout.setHorizontalGroup(layout.createParallelGroup()
					.addGroup(layout.createSequentialGroup()
							.addComponent(addButton)
							.addComponent(removeButton))
					.addComponent(listScroll)
					.addGroup(layout.createSequentialGroup()
							.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
									.addComponent(depthLabel)
									.addComponent(offsetLabel)
									.addComponent(parallaxLabel)
									.addComponent(colorLabel))
							.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
									.addComponent(setTextureButton)
									.addComponent(depthSpinner)
									.addGroup(layout.createSequentialGroup()
											.addComponent(offsetXSpinner)
											.addComponent(offsetYSpinner))
									.addGroup(layout.createSequentialGroup()
											.addComponent(parallaxXSpinner)
											.addComponent(parallaxYSpinner))
									.addGroup(layout.createSequentialGroup()
											.addComponent(colorChooser)
											.addComponent(alphaLabel)
											.addComponent(alphaSpinner)))));
			layout.setVerticalGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(addButton)
							.addComponent(removeButton))
					.addComponent(listScroll)
					.addComponent(setTextureButton)
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(offsetLabel)
							.addComponent(offsetXSpinner)
							.addComponent(offsetYSpinner))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(parallaxLabel)
							.addComponent(parallaxXSpinner)
							.addComponent(parallaxYSpinner))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(depthLabel)
							.addComponent(depthSpinner))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(colorLabel)
							.addComponent(colorChooser)
							.addComponent(alphaLabel)
							.addComponent(alphaSpinner)));

			// a hack to account for border sizes
			Insets insets = getInsets();
			setMinimumSize(new Dimension(320 + insets.left + insets.right, getMinimumSize().height));
			setSize(320 + insets.left + insets.right, 768 + insets.top + insets.bottom);

			addButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					if (!performCallbacks || commandsPending > 0) {
						return;
					}
					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							// create the background layer
							final RoomDefinition.BackgroundLayerDefinition def =
									new RoomDefinition.BackgroundLayerDefinition();
							final BackgroundLayerInstance instance =
									new BackgroundLayerInstance(def.getBackgroundLayer(), null, getEngine().window.renderer);
							roomDefinition.addBackgroundLayer(roomDefinition.getBackgroundLayerCount(), def);
							((RoomState) getGameState()).addBackgroundLayer(instance);

							finishedCommand(new Runnable() {
								@Override
								public void run() {
									// add to the list
									Node node = new Node();
									node.text = "Layer";
									node.layer = instance;

									boolean tempPerformCallbacks = performCallbacks;
									performCallbacks = false;
									try {
										model.addElement(node);
									} finally {
										performCallbacks = tempPerformCallbacks;
									}
									list.setSelectedIndex(model.getSize()-1);
								}
							});
						}
					});
				}
			});

			removeButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					if (!performCallbacks || commandsPending > 0) {
						return;
					}
					if (listSelection < 0) {
						return;
					}
					// first enqueues the "deselect" command
					// current selection is stored, then removed
					final int oldSelection = listSelection;
					list.clearSelection();

					final Node node = model.get(listSelection);
					final BackgroundLayerInstance instance = node.layer;
					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							// remove the background layer
							((RoomState) getGameState()).removeBackgroundLayer(instance);
							// remove from definition
							for (int i = 0; i < roomDefinition.getBackgroundLayerCount(); ++i) {
								if (roomDefinition.getBackgroundLayer(i).getBackgroundLayer() == instance.getBackgroundLayer()) {
									roomDefinition.removeBackgroundLayer(i);
									break;
								}
							}

							finishedCommand(new Runnable() {
								@Override
								public void run() {
									model.remove(oldSelection);
								}
							});
						}
					});
				}
			});

			list.addListSelectionListener(new ListSelectionListener() {
				@Override
				public void valueChanged(ListSelectionEvent e) {
					if (!performCallbacks) {
						return;
					}
					if (commandsPending > 0) {
						// revert to previous selection, as if nothing happened
						boolean tempPerformCallbacks = performCallbacks;
						performCallbacks = false;
						try {
							list.setSelectedIndex(listSelection);
						} finally {
							performCallbacks = tempPerformCallbacks;
						}
						return;
					}

					final int depth = (Integer) depthSpinner.getValue();
					final int offsetX = (Integer) offsetXSpinner.getValue();
					final int offsetY = (Integer) offsetYSpinner.getValue();
					final float parallaxX = (Float) parallaxXSpinner.getValue();
					final float parallaxY = (Float) parallaxYSpinner.getValue();
					final float alpha = (Float) alphaSpinner.getValue();

					final BackgroundLayerInstance oldInstance = (listSelection < 0) ?
							null : model.get(listSelection).layer;
					final BackgroundLayerInstance newInstance = (list.getSelectedIndex() < 0) ?
							null : model.get(list.getSelectedIndex()).layer;
					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							if (oldInstance != null) {
								oldInstance.getBackgroundLayer().setDepth(depth);
								oldInstance.getBackgroundLayer().setOffset(offsetX, offsetY);
								oldInstance.getBackgroundLayer().setParallax(parallaxX, parallaxY);
								Vector4f color = oldInstance.getBackgroundLayer().getColor();
								color.w(alpha);
								oldInstance.getBackgroundLayer().setColor(color);
							}

							// retrieve the new values
							final int depth = (newInstance == null) ? 0 : newInstance.getBackgroundLayer().getDepth();
							final int offsetX = (newInstance == null) ? 0 : newInstance.getBackgroundLayer().getOffsetX();
							final int offsetY = (newInstance == null) ? 0 : newInstance.getBackgroundLayer().getOffsetY();
							final float parallaxX = (newInstance == null) ? 0.0f : newInstance.getBackgroundLayer().getParallaxX();
							final float parallaxY = (newInstance == null) ? 0.0f : newInstance.getBackgroundLayer().getParallaxY();
							final Vector4f color = (newInstance == null) ? new Vector4f() : newInstance.getBackgroundLayer().getColor();

							// variable set in next line is not actually used:
							//activeBackgroundLayer = newInstance;

							finishedCommand(new Runnable() {
								@Override
								public void run() {
									listSelection = list.getSelectedIndex();
									boolean tempPerformCallbacks = performCallbacks;
									performCallbacks = false;
									try {
										depthSpinner.setValue(depth);
										offsetYSpinner.setValue(offsetX);
										offsetXSpinner.setValue(offsetY);
										parallaxXSpinner.setValue(parallaxX);
										parallaxYSpinner.setValue(parallaxY);
										colorChooser.setBackground(new Color(color.x(), color.y(), color.z()));
									} finally {
										performCallbacks = tempPerformCallbacks;
									}
								}
							});
						}
					});
				}
			});

			setTextureButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					if (!performCallbacks || commandsPending > 0 || listSelection < 0) {
						return;
					}

					final String result = (String) JOptionPane.showInputDialog(BackgroundEditorFrame.this,
							"Choose texture:", "Choose Texture", JOptionPane.PLAIN_MESSAGE, null,
							textureNames, null);
					if (result == null) {
						return;
					}

					final BackgroundLayerInstance instance = model.get(listSelection).layer;
					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							GpuTexture texture = ((RoomState) getGameState()).getCoreState().resourceManager
									.getTexture(
											result);
							if (texture != null) {
								instance.setTexture(texture);
								// set definition texture
								for (int i = 0; i < roomDefinition.getBackgroundLayerCount(); ++i) {
									if (roomDefinition.getBackgroundLayer(i).getBackgroundLayer() == instance.getBackgroundLayer()) {
										roomDefinition.getBackgroundLayer(i).setTexture(result);
										break;
									}
								}
							}

							finishedCommand();
						}
					});
				}
			});

			ChangeListener changeListener = new ChangeListener() {
				@Override
				public void stateChanged(ChangeEvent e) {
					if (!performCallbacks || commandsPending > 0 || listSelection < 0) {
						return;
					}

					final int depth = (Integer) depthSpinner.getValue();
					final int offsetX = (Integer) offsetXSpinner.getValue();
					final int offsetY = (Integer) offsetYSpinner.getValue();
					final float parallaxX = (Float) parallaxXSpinner.getValue();
					final float parallaxY = (Float) parallaxYSpinner.getValue();
					final float alpha = (Float) alphaSpinner.getValue();
					final BackgroundLayerInstance instance = model.get(listSelection).layer;
					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							instance.getBackgroundLayer().setDepth(depth);
							instance.getBackgroundLayer().setOffset(offsetX, offsetY);
							instance.getBackgroundLayer().setParallax(parallaxX, parallaxY);
							Vector4f color = instance.getBackgroundLayer().getColor();
							color.w(alpha);
							instance.getBackgroundLayer().setColor(color);
							finishedCommand();
						}
					});
				}
			};

			depthSpinner.addChangeListener(changeListener);
			offsetXSpinner.addChangeListener(changeListener);
			offsetYSpinner.addChangeListener(changeListener);
			parallaxXSpinner.addChangeListener(changeListener);
			parallaxYSpinner.addChangeListener(changeListener);
			alphaSpinner.addChangeListener(changeListener);
			colorChooser.addMouseListener(new MouseListener() {
				@Override
				public void mouseClicked(MouseEvent e) {
					if (!performCallbacks || commandsPending > 0 || listSelection < 0) {
						return;
					}
					if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 1) {
						final Color result = JColorChooser.showDialog(BackgroundEditorFrame.this, "Choose Color", Color.WHITE);
						if (result == null) {
							return;
						}
						colorChooser.setBackground(result);

						final BackgroundLayerInstance instance = model.get(listSelection).layer;
						enqueueCommand(new Runnable() {
							@Override
							public void run() {
								Vector4f color = instance.getBackgroundLayer().getColor();
								color.x((float) result.getRed() / 255.0f);
								color.y((float) result.getGreen() / 255.0f);
								color.z((float) result.getBlue() / 255.0f);
								instance.getBackgroundLayer().setColor(color);
								finishedCommand();
							}
						});
					}
				}
				public void mousePressed(MouseEvent e) {}
				public void mouseReleased(MouseEvent e) {}
				public void mouseEntered(MouseEvent e) {}
				public void mouseExited(MouseEvent e) {}
			});

			listSelection = -1;

			performCallbacks = true;

			setVisible(true);
		}

		private void init() {
			RoomState roomState = (RoomState) getGameState();
			// make a copy to avoid concurrent modification errors
			final Set<BackgroundLayerInstance> backgroundLayers = new HashSet<BackgroundLayerInstance>();
			backgroundLayers.addAll(roomState.getBackgroundLayers());

			SwingUtilities.invokeLater(new Runnable() {
				@Override
				public void run() {
					performCallbacks = false;
					try {
						for (BackgroundLayerInstance layer : backgroundLayers) {
							Node node = new Node();
							node.layer = layer;
							node.text = "Layer";
							model.addElement(node);
						}
					} finally {
						performCallbacks = true;
					}
				}
			});
		}

		private int listSelection;

		private int commandsPending = 0;
		private void enqueueCommand(Runnable command) {
			commandQueue.add(command);
			++commandsPending;
		}
		private void finishedCommand() {
			finishedCommand(null);
		}
		private void finishedCommand(final Runnable uiCommand) {
			SwingUtilities.invokeLater(new Runnable() {
				@Override
				public void run() {
					--commandsPending;
					if (uiCommand != null) {
						uiCommand.run();
					}
				}
			});
		}
	}
}
